﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using API;
using libmp;
using mb;
using CacheStream = libmp.CacheStream;

namespace libmp
{
    using OfflineItem = ValueTuple<int, long, long>;
    public class SimpleVideoServer
    {
        static string DebugLogTags = "debug";
        static void DebugLog(string tag, params object[] logs)
        {
            if( DebugLogTags.IndexOf(tag) < 0 ) return;
            logger.Log(logs);
        }

        static ILog logger = App.logger;
        private Thread _serverThread;
        private HttpListener _listener;
        private int port;
        string address;
        public static string CachePath;

        public int Port
        {
            get { return port; }
        }

        public SimpleVideoServer()
        {
            CachePath = Xamarin.Essentials.FileSystem.CacheDirectory + "/movie/";

            var so = mb.ServiceLocator.Instance.Get<IStartOption>();
            if( so.GetBoolValue("deleteOfflineCache") && Directory.Exists(CachePath) ) {
                Directory.Delete(CachePath, true);
            }

            //get an empty port
            TcpListener l = new TcpListener(IPAddress.Loopback, 0);
            l.Start();
            port = ( (IPEndPoint) l.LocalEndpoint ).Port;
            address = ( (IPEndPoint) l.LocalEndpoint ).Address.ToString();
            l.Stop();

            _serverThread = new Thread(this.Listen);
            _serverThread.Start();

        }

        public string GetVideoUrl(int vid)
        {
            return $"http://{address}:{port}/video/{vid}";
        }

        public void DeleteVideoCache(int vid)
        {
            var path = CachePath + vid;
            try {
                Directory.Delete(path, true);
            }
            catch( Exception e ) {
                logger.Log("delete video cache fail", path, e);
            }
        }

        public List<(int vid, long total, long now)> GetOfflineItemInfoList()
        {
            if( !Directory.Exists(CachePath) )
                return new List<(int vid, long total, long now)>();

            var subDirs = Directory.GetFileSystemEntries(CachePath);
            logger.Log("offline videos", subDirs.Length, string.Join("\n", subDirs));
            var ret = subDirs.Select(dir =>
            {
                try {
                    int vid = int.Parse(dir.Substring(dir.LastIndexOf('/') + 1));
                    var localPath = dir + "/length";
                    long totalLength;
                    using( var stream = new BinaryReader(File.OpenRead(localPath)) ) {
                        totalLength = stream.ReadInt64();
                    }
                    var segs = Directory.GetFileSystemEntries(dir);
                    long curLength = ( segs.Length - 1 ) * CacheStream.SegmentSize;
                    if( curLength > totalLength ) curLength = totalLength;
                    logger.Log("video", dir, segs.Length);
                    return (vid, totalLength, curLength);
                }
                catch( Exception e ) {
                    logger.Log("read offline item error", e);
                    return (0, 0, 0);
                }
            }).ToList();
            ret.RemoveAll(v => v.Item1 == 0);
            return ret;

        }

        public void Stop()
        {
            _serverThread.Abort();
            _listener.Stop();
        }

        private void Listen()
        {
            var url = $"http://*:{port}/";
            _listener = new HttpListener();
            _listener.Prefixes.Add(url);
            _listener.Start();
            while( true ) {
                try {
                    DebugLog("debug", "http listening.....", url);
                    HttpListenerContext context = _listener.GetContext();
                    DebugLog("debug", "http accept.....");
                    Process(context);
                }
                catch( Exception ex ) {
                    DebugLog("debug", "http error.....", ex);

                }
            }
        }

        private void Process(HttpListenerContext context)
        {
            string path = context.Request.Url.AbsolutePath;
            // Console.WriteLine("HTTP PROCESS", filename);
            path = path.Substring(1);

            if( path == "check" ) {
                var str = "i'm fine";
                var buf = Encoding.ASCII.GetBytes(str);
                context.Response.StatusCode = (int) HttpStatusCode.OK;
                context.Response.ContentLength64 = buf.Length;
                context.Response.OutputStream.Write(buf, 0, buf.Length);
                context.Response.OutputStream.Close();
                return;
            }

            else if( path.StartsWith("video") ) {
                ProcessMovie(context);
                return;
            }

            context.Response.OutputStream.Close();
        }

        CacheStream LastStream;
        int LastVid;

        public void CloseCurrentStream()
        {
            logger.Log("CloseCurrentStream", LastVid);
            LastStream?.Close();
            LastStream = null;
            LastVid = 0;
        }

        async Task<CacheStream> GetStream(int vid)
        {
            if( vid == LastVid )
                return LastStream;
            else if( LastStream != null ) {
                LastStream.Close();
            }

            var movieInfo = DataStore.store.GetVideoItemById(vid);
            var url = movieInfo.vpath;
            var stream = new CacheStream(url, CachePath + vid);
            await stream.Open();
            LastVid = vid;
            LastStream = stream;
            return stream;
        }

        int requestId = 0;
        private async void ProcessMovie(HttpListenerContext context)
        {
            int curRequestId = requestId++;
            string hrange = context.Request.Headers.Get("range");
            string hall = string.Join(",", context.Request.Headers.AllKeys);

            string url = context.Request.Url.AbsolutePath;
            int vid = int.Parse(url.Substring(url.LastIndexOf('/') + 1));
            var movieInfo = DataStore.store.GetVideoItemById(vid);
            movieInfo.vpath = movieInfo.vpath.Replace("54.255.234.78:20729/video", "mj.19v5.com:5000");
            var keyBuffer = System.Text.Encoding.ASCII.GetBytes(movieInfo.vkey);

            libmp.CacheStream input = null;
            try {
                input = await GetStream(vid);
                long start = 0;
                long end = input.GetLength() - 1;
                long remain = end - start + 1;
                if( hrange != null ) {
                    hrange = hrange.Substring("bytes=".Length);
                    var range = hrange.Split("-");
                    start = int.Parse(range[0]);
                    if( range.Length > 0 && range[1] != "" )
                        end = int.Parse(range[1]);

                    remain = end - start + 1;
                    context.Response.StatusCode = (int) HttpStatusCode.PartialContent;
                    context.Response.ContentLength64 = remain;
                    context.Response.AddHeader("Content-Range", $"bytes {start}-{end}/{input.GetLength()}");
                }
                else {
                    context.Response.StatusCode = (int) HttpStatusCode.OK;
                    context.Response.ContentLength64 = input.GetLength();
                }

                context.Response.ContentType = "video/mp4";
                DebugLog("debug", $"{curRequestId}.httpmovie start! {start}-{end}/{input.GetLength()} {url}");

                byte[] buffer = new byte[1024 * 512 * 1];
                int nbytes;
                while( remain > 0 ) {
                    int errCount = 0;
                    while( true ) {
                        try {
                            DebugLog("debug", $"{curRequestId}.httpmovie read--->> {start}");
                            nbytes = await input.Read(start, buffer, 0, buffer.Length);
                            DebugLog("debug", $"{curRequestId}.httpmovie read<<--- {nbytes}");
                            break;
                        }
                        catch( IOException e ) {
                            DebugLog("debug", $"{curRequestId}.httpmovie readinput io erro", start, ++errCount);
                            await Task.Delay(1000);
                        }
                        catch( Exception e ) {
                            DebugLog("debug", $"{curRequestId}.httpmovie readinput erro", start, ++errCount);
                            DebugLog("debug", $"{curRequestId}.excep1", e);
                            DebugLog("debug", $"{curRequestId}.excep2", e.Data["StackTrace"]?.ToString());
                            await Task.Delay(1000);
                            // context.Response.OutputStream.Close();
                            // return;
                        }
                    }

                    //-///////////////////////
                    // long koff = start;
                    // for (int i = 0; i < nbytes; i++, koff++)
                    //     buffer[i] = (byte) (buffer[i] ^ keyBuffer[koff % keyBuffer.Length]);
                    //-///////////////////////
                    mb.Util.BufferFlip(buffer, nbytes, keyBuffer, start);

                    try {
                        int needSend = Math.Min(nbytes, (int) remain);
                        DebugLog("debug", $"{curRequestId}.httpmovie sent -->> {needSend}/{remain}");
                        var taskTimeout = Task.Delay(TimeSpan.FromMilliseconds(1000));
                        context.Response.OutputStream.Write(buffer, 0, needSend);
                        DebugLog("debug", $"{curRequestId}.httpmovie sent <<--");
                    }
                    catch( IOException ) {
                        DebugLog("debug", $"{curRequestId}.httpmovie sent close", hrange);
                        context.Response.OutputStream.Close();
                        return;
                    }

                    // DebugLog("debug",$"{curRequestId}.httpmovie sent: {start}");
                    remain -= nbytes;
                    start += nbytes;
                }
                context.Response.OutputStream.Close();
                DebugLog("debug", $"{curRequestId}.httpmovie sent finish");
            }
            catch( Exception ex ) {
                DebugLog("debug", $"{curRequestId}.httpmovie sent error", ex);
                context.Response.OutputStream.Close();
                return;
            }
            finally {
                input?.Close();
            }

        }
    }
}